18 键/值编码

18.1 入门项目

说明:

18.2 KVC简介

说明:通过键/值编码(KVC),没有相应getter方法也能获取属性值,没有相应setter方法也能设置属性值。
自动开箱和装箱:KVC具有这种功能,常规方法调用和属性语法不具备该功能。

valueForKey实例方法

说明:将属性名作为获取属性的
原理:Objective-C的运行中使用元数据打开对象并进入其中查找需要的信息,查找顺序为

  1. 以参数命名(keyisKey)的getter
  2. 名称为_keykey的实例变量

自动装箱:通过该方法获取属性值时,如果属性为标量(int、float、struct),该方法会根据需要将属性放入NSNumberNSValue中。
兼容性:CC++中不能执行这种操作。
原型:NSKeyValueCoding.h

1
2
3
4
5
/**
* @param {NSString *} key 属性名
* @return {id} 属性值(标量会被自动装箱)
*/

- (nullable id)valueForKey:(NSString *)key;

setValue:forkey实例方法

说明:将属性名作为设置属性的
自动开箱:使用该方法设置属性时,如果属性是标量(int、float、struct),则会从新值(id)中提取出和属性类型相符的标量
技巧:如果想设置一个标量值,在调用setValue方法之前需要将它们包装掐来(封装到对象中)。
原型:NSKeyValueCoding.h

1
2
3
4
5
/**
* @param {id} value 要设置的值(需要的话会被开箱)
* @param {NSString *} key 属性名
*/

- (void)setValue:(nullable id)value forKey:(NSString *)key;
1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
25
26
27
28
29
30
31
32
33
34
35
36
37
38
39
40
41
 // 创建 Car 对象
Car *car = [[Car alloc] init];
car.name = @"Herbie";
car.make = @"Honda";
car.model = @"CRX";
car.modelYear = 1984;
car.numberOfDoors = 2;
car.mileage = 110000;
// 初始化轮胎
for (int i = 0; i < 4; i++)
{
AllWeatherRadial *tire;
tire = [[AllWeatherRadial alloc] init];
[car setTire: tire atIndex:i];
}
// 初始化引擎
Slant6 *engine = [[Slant6 alloc] init];
car.engine = engine;

// 通过 KVC 访问车的引擎的马力
NSLog(@"horsepower is %@", [engine valueForKey:@"horsepower"]);
// 通过 KVC 重新设置车的引擎的马力
[engine setValue:[NSNumber numberWithInt:150] forKey:@"horsepower"];
NSLog(@"horsepower is %@", [engine valueForKey:@"horsepower"]);
NSLog(@"Car is %@", car);

// 通过 KVC 访问车的名字
NSString *name = [car valueForKey:@"name"];
NSLog(@"%@", name);
// 通过 KVC 访问车的制造商
NSLog(@"make is %@", [car valueForKey:@"make"]);
// 通过 KVC 访问车的车型出厂日期
NSLog(@"model year is %@", [car valueForKey:@"modelYear"]);

// 通过 KVC 设置车名
[car setValue:@"Harold" forKey:@"name"];
NSLog(@"new car name is %@", [car name]);

// 通过 KVC 设置里程数
[car setValue:[NSNumber numberWithFloat:25062.4] forKey:@"mileage"];
NSLog(@"new mileage is %.1f", [car mileage]);

18.3 键路径

说明:进行getset操作,除了通过,还可以通过键路径键路径可以根据对象图访问到任意深度的对象,比使用一系列嵌套方法调用更容易访问到对象。

valueForKeyPath实例方法

说明:将属性名作为获取属性的
原型:NSKeyValueCoding.h

1
2
3
4
5
/**
* @param {NSString *} keyPath 键路径
* @return {id} 属性值(标量会被自动装箱)
*/

- (nullable id)valueForKeyPath:(NSString *)keyPath;

setValue:forKeyPath实例方法

说明:将属性名作为设置属性的
原型:NSKeyValueCoding.h

1
2
3
4
5
/**
* @param {id} value 要设置的值(需要的话会被开箱)
* @param {NSString *} keyPath 键路径
*/

- (void)setValue:(nullable id)value forKeyPath:(NSString *)keyPath;
1
2
3
4
// 通过 KVC 设置车的引擎的马力
[car setValue:[NSNumber numberWithInt:155] forKeyPath:@"engine.horsepower"];
// 通过 KVC 访问车的引擎的马力
NSLog(@"horsepower is %@", [car valueForKeyPath:@"engine.horsepower"]);

18.4 整体操作

说明:如果使用键值键路径访问位于对象中的数组类型的实例属性的元素,实际上会对数组中所有元素进行操作。如果是查询操作,则还会将查询结果打包到另一个数组中并返回。
注意:这种整体操作意味着无法在键路径中单独索引数组类型的属性的其中一个元素。

1
2
3
// 通过 KVC 访问车的轮胎的胎压,会便利tires的每个属性,并将所有tires的属性的pressure变量封装到 NSNumber 对象中并返回
NSArray *pressures = [car valueForKeyPath:@"tires.pressure"];
NSLog(@"pressures %@", pressures);

18.4.1 休息一下

说明:创建一个新的类用于后面的学习使用。

Garage.h

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
#import <Cocoa/Cocoa.h>

@class Car;

@interface Garage : NSObject {
NSString *name;
NSMutableArray *cars;
NSMutableDictionary *stuff;
}

@property (readwrite, copy) NSString *name;

- (void) addCar: (Car *) car;

- (void) print;

@end // Garage

Garage.m

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
25
26
27
28
29
30
31
32
33
34
35
36
37
38
39
40
41
42
43
#import "Garage.h"

@implementation Garage

@synthesize name;

- (void) addCar: (Car *) car {
if (cars == nil) {
cars = [[NSMutableArray alloc] init];
}
[cars addObject: car];

} // addCar

- (void) dealloc {
[name release];
[cars release];
[stuff release];
[super dealloc];
} // dealloc

- (void) print {
NSLog (@"%@:", name);

for (Car *car in cars) {
NSLog (@" %@", car);
}

} // print

- (void) setValue: (id) value forUndefinedKey: (NSString *) key {
if (stuff == nil) {
stuff = [[NSMutableDictionary alloc] init];
}
[stuff setValue: value forKey: key];
} // setValueForUndefinedKey

- (id) valueForUndefinedKey:(NSString *)key {
id value = [stuff valueForKey: key];
return (value);
} // valueForUndefinedKey

@end // Car

main.m

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
// 汽车集合
Garage *garage = [[Garage alloc] init];
garage.name = @"Joe's Garage";
// 创建一些汽车实例
Car *car;
car = makeCar(@"Herbie", @"Honda", @"CRX", 1984, 2, 110000, 58);
[garage addCar:car];
car = makeCar(@"Badger", @"Acura", @"integra", 1987, 5, 217036, 130);
[garage addCar:car];
car = makeCar(@"Elvis", @"Acura", @"Legend", 1989, 4, 28123.4, 151);
[garage addCar:car];
car = makeCar(@"Phoenix", @"Pontiac", @"Firebird", 1969, 2, 85128.3, 345);
[garage addCar:car];

// 观察汽车集合
[garage print];

18.4.2 快速运算

说明:键路径中可以使用进行引用一些运算符进行一些运算,比如

  • prevProperty.@count计算 prevProperty 包含的对象的总数
  • prevProperty.@sum.nextProperty:计算prevProperty包含的所有对象的nextProperty属性的总和
  • prevProperty.@avg.nextProperty:计算prevProperty包含的所有对象的nextProperty属性的平均值
  • prevProperty.@max.nextProperty:计算prevProperty包含的所有对象的nextProperty属性的最大值
  • prevProperty.@min.nextProperty:计算prevProperty包含的所有对象的nextProperty属性的最小值
  • prevProperty.@distinctUnionOfObjects.nextProperty:获得从prevProperty包含的所有对象的nextProperty属性构成的集合去重后的集合

注意:不要滥用KVC通过键路径提供的处理集合类的快速运算特性,因为

  • 速度比较慢
  • 编译器无法对它进行错误检查(出现运行时错误时才能发觉)

限制:无法添加自定义的运算符

1
2
3
4
5
6
7
8
9
10
11
12
 // @count
NSNumber *count = [garage valueForKeyPath:@"cars.@count"];
// @sum
NSNumber *sum = [garage valueForKeyPath:@"cars.@sum.mileage"];
// @avg
NSNumber *avgMileage = [garage valueForKeyPath:@"cars.@avg.mileage"];
// @max、 @min
NSNumber *min, *max;
max = [garage valueForKeyPath:@"cars.@max.mileage"];
min = [garage valueForKeyPath:@"cars.@max.mileage"];
// @distinctUnionOfObjects
NSArray *manufacture = [garage valueForKeyPath:@"cars.@distinctUnionOfObjects.make"];

18.5 批处理

说明:KVC提供了对对象进行批量操作的方式。

setValuesForKeysWithDictionary实例方法

说明:通过字典对对象进行批量set
原型:NSKeyValueCoding.h

1
2
3
4
/**
* @param {NSDictionary<NSString *, id> *} keyedValues 包含一系列属性名和值的字典
*/

- (void)setValuesForKeysWithDictionary:(NSDictionary<NSString *, id> *)keyedValues;

dictionaryWithValuesForKeys实例方法

说明:通过数组对对象进行批量set
原型:NSKeyValueCoding.h

1
2
3
4
5
/**
* @param {NSArray<NSString *> *} keys 包含要获取的属性的属性名
* @return {NSDictionary<NSString *, id> *} 包含要获取的属性名/属性值对的字典
*/

- (NSDictionary<NSString *, id> *)dictionaryWithValuesForKeys:(NSArray<NSString *> *)keys;
1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
// 获取汽车集合中的最后一个汽车实例
car = [[garage valueForKeyPath:@"cars"] lastObject];
// 创建一个数组:包含创建字典需要的键集合
NSArray *keys = [NSArray arrayWithObjects:@"make", @"model", @"modelYear", nil];
// 使用 KVC 批量get:利用上一步创建的包含一系列键的数组
NSDictionary *carValues = [car dictionaryWithValuesForKeys:keys];
// 创建字典:用于初始化汽车实例的属性
NSDictionary *newValues = [NSDictionary dictionaryWithObjectsAndKeys:
@"Chevy", @"make",
@"Nova",@"model",
[NSNumber numberWithInt:1964], @"modelYear",
[NSNull null], @"mileage",
nil];
// 使用 KVC 批量set汽车
[car setValuesForKeysWithDictionary:newValues];
NSLog(@"car with new values is %@", car);

18.6 nil 仍然可用

背景:默认情况下,应用KVC调用setValue:forKey设置某个属性的值为nil时,编译器会给出警告。
解决:可以通过重写setNilValueForKey方法给出有意义个返回值而避免警告。
扩展:这里的 nil是标量nil,不要和[NSNull null]相混淆

1
2
3
4
5
6
7
- (void) setNilValueForKey: (NSString *) key {
if ([key isEqualToString: @"mileage"]) {
mileage = 0;
} else {
[super setNilValueForKey: key];
}
} // setNilValueForKey
1
2
// 会将 mileage 设置为 0
[car setValue: nil forKey: @"mileage"];

18.7 处理未定义键

背景:如果KVC机制无法找到处理方法,会退回并询问该如何处理。默认的实现会取消操作。
解决:通过重写setValue:forUndefinedKeyvalueForUndefinedKey更改默认行为,使对象可以设置和获取任何键。

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
25
26
27
28
29
30
/**
* @override
* 处理通过 KVC 设置不存在的属性的值
*
* @param value 要设置的值
* @param key 要设置的属性名
*/

- (void) setValue:(id)value forUndefinedKey: (NSString *) key
{
// 惰性初始化
if (stuff == nil)
{
stuff = [[NSMutableDictionary alloc] init];
}
[stuff setValue:value forKey:key];
}// setValueForUndefinedKey

/**
* @override
* 处理通过 KVC 方式获取不存在的属性的值
*
* @param key 属性名
*
* @return 试图获取不存在的属性的值时的返回值
*/

- (id) valueForUndefinedKey:(NSString *)key
{
id value = [stuff valueForKey:key];
return (value);
}// valueForUnderfinedKey

18.8 小结